use std::{collections::HashSet, fs::File, io::BufReader, path::Path};

use serde::{Deserialize, Serialize};

use ggez::{
    event::{self, EventHandler},
    glam::*,
    graphics::{self, Canvas, Color, Mesh},
    input::keyboard::KeyCode,
    Context, ContextBuilder, GameResult,
};

const NUM_COLS: usize = 10; //20; //40;
const NUM_ROWS: usize = 8; //16; //32;
const NODE_SIZE: f32 = 20.0;
const NUM_NODES: usize = NUM_COLS * NUM_ROWS;

const WINDOW_WIDTH: usize = 800;
const WINDOW_HEIGHT: usize = 600;

const MAX_UNITS: usize = (WINDOW_WIDTH / NUM_COLS) * (WINDOW_HEIGHT / NUM_ROWS);

struct Weights {}
impl Weights {
    pub const PATH: f32 = 1.0;
    pub const GRASS: f32 = 2.0;
    pub const WATER: f32 = 3.0;
}

#[derive(Debug, Serialize, Deserialize)]
struct Astar {
    nodes: Vec<Node>,
    start_index: usize,
    end_index: usize,
    open_set: HashSet<usize>, // open set containing indexes of nodes in sorted order
    closed_set: Vec<usize>,
    final_path: Vec<usize>, // the final path -- a list of indexes into the nodes array.
                            // empty if path not found
}

#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
struct Node {
    parent_idx: usize,
    weight: f32, // weight of this node which affects g-score
    f: f32,
    g: f32,
    h: f32,
}

impl Astar {
    fn new() -> Self {
        let n = vec![
            Node {
                parent_idx: 0,
                weight: Weights::GRASS * MAX_UNITS as f32, // initialize to full of Grass
                f: f32::INFINITY,
                g: f32::INFINITY,
                h: f32::INFINITY,
            };
            NUM_NODES
        ];

        Astar {
            nodes: n,
            start_index: 0,
            end_index: 0,
            final_path: Vec::new(),
            open_set: HashSet::new(),
            closed_set: Vec::new(),
        }
    }

    // returns 2D array indexes from 1D index
    fn index_to_2D(node_index: usize) -> (usize, usize) {
        let x = node_index % NUM_COLS;
        let y = node_index / NUM_COLS;
        // println!("index {} -> ({}, {})", node_index, x, y);
        (x, y)
    }

    // returns the (x, y) coordinate of a given node
    fn get_center_of_node(node_index: usize) -> (f32, f32) {
        let (x_index, y_index) = Astar::index_to_2D(node_index);
        let node_width = (WINDOW_WIDTH / NUM_COLS) as f32;
        let node_height = (WINDOW_HEIGHT / NUM_ROWS) as f32;

        let x_offset = node_width / 2.0;
        let y_offset = node_height / 2.0;

        (
            x_index as f32 * node_width + x_offset,
            y_index as f32 * node_height + y_offset,
        )
    }

    // get index of 1D array using 2D position
    // NOTE: NOT the same (x, y) as the game positions
    fn index(x: usize, y: usize) -> Option<usize> {
        println!("index function, 2D indices ({}, {})", x, y);
        if x >= NUM_COLS || y >= NUM_ROWS {
            return None;
        }
        Some(y * NUM_COLS + x)
    }

    fn heuristic(&self, index: usize) -> f32 {
        // calculate Manhattan distance to end node
        let (x_end_node, y_end_node) = Astar::get_center_of_node(self.end_index);
        let (x, y) = Astar::get_center_of_node(index);

        return (x_end_node - x).abs() + (y_end_node - y).abs();
    }

    // run the A* algorithm
    fn run(&mut self) {
        while !self.open_set.is_empty() {
            let mut lowest_f = f32::INFINITY;
            let mut q_index = 0;
            // get q_index which points to the node with the lowest f score in the open set
            for &i in &self.open_set {
                // println!("{}", i);
                if self.nodes[i].f < lowest_f {
                    lowest_f = self.nodes[i].f;
                    q_index = i;
                }
            }

            self.open_set.remove(&q_index);

            // look at neighbors for q and set their parents to q
            for x in [-1, 0, 1] {
                for y in [-1, 0, 1] {
                    // skip self
                    if x == 0 && y == 0 {
                        continue;
                    }

                    println!("q_index: {}", q_index);

                    let (q_x, q_y) = Astar::index_to_2D(q_index);
                    let neighbor_x = q_x as isize + x;
                    let neighbor_y = q_y as isize + y;

                    if neighbor_x < 0
                        || neighbor_x >= NUM_COLS as isize
                        || neighbor_y < 0
                        || neighbor_y >= NUM_ROWS as isize
                    {
                        continue;
                    }

                    if let Some(neighbor_index) =
                        Astar::index(neighbor_x as usize, neighbor_y as usize)
                    {
                        //let neighbor = &mut self.nodes[neighbor_index];
                        self.nodes[neighbor_index].parent_idx = q_index;

                        // found end!
                        if self.end_index == neighbor_index {
                            todo!("Implement way to draw final path");
                            break;
                        } else {
                            // compute g and h
                            let tentative_g =
                                self.nodes[q_index].g + self.nodes[neighbor_index].weight;
                            let tentative_h = self.heuristic(neighbor_index);
                            let tentative_f = tentative_g + tentative_h;

                            if !self.open_set.contains(&neighbor_index)
                                || tentative_f < self.nodes[neighbor_index].f
                            {
                                println!("Adding to open set: {}", neighbor_index);
                                self.open_set.insert(neighbor_index);
                                self.nodes[neighbor_index].g = tentative_g;
                                self.nodes[neighbor_index].h = tentative_h;
                                self.nodes[neighbor_index].f = tentative_f;
                            }
                        }
                    } else {
                        continue;
                    }
                }
            }
            // push q to closed list
            self.closed_set.push(q_index);
        }
    }
}
struct Palette {}
impl Palette {
    pub fn green() -> Color {
        Color::from((0x53, 0x79, 0x4e))
    }
    pub fn light_green() -> Color {
        Color::from((0x55, 0xc1, 0x53))
    }
    pub fn blue() -> Color {
        Color::from((0x00, 0x77, 0xB4))
    }
    pub fn red() -> Color {
        Color::from((0xB5, 0x4f, 0x59))
    }
    pub fn brown() -> Color {
        Color::from((0x8b, 0x8c, 0x56))
    }
}

#[derive(Debug, Serialize, Deserialize)]
struct Game {
    objects: Vec<Object>,
    astar: Astar,
    key_pressed: bool,
    current_object_type: ObjectType,
    mouse_held_down: bool,
    mouse_over_object: Option<(ObjectType, f32, f32)>,
    should_draw_nodes: bool,
}

// Object is a square with side length `size` centered at `pos`
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
struct Object {
    object_type: ObjectType,
    pos: (f32, f32), // (x, y)
    size: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
enum ObjectType {
    Path,
    Start,
    End,
    FinalPath,
    Water,
}

impl Object {
    // checks if point (x, y) intersects with this Object
    fn intersects(&self, x: f32, y: f32) -> bool {
        let (obj_x, obj_y) = self.pos;
        let obj_x_max = obj_x + self.size;
        let obj_y_max = obj_y + self.size;

        if x >= obj_x && x <= obj_x_max && y >= obj_y && y <= obj_y_max {
            return true;
        }
        false
    }
}

impl Game {
    fn new(_ctx: &mut Context) -> Game {
        Game {
            objects: Vec::new(),
            astar: Astar::new(),
            key_pressed: false,
            current_object_type: ObjectType::Start,
            mouse_held_down: false,
            mouse_over_object: None,
            should_draw_nodes: false,
        }
    }

    fn index_from_position(x: f32, y: f32) -> Option<usize> {
        let index_x = (x as usize) / (WINDOW_WIDTH / NUM_COLS);
        let index_y = (y as usize) / (WINDOW_HEIGHT / NUM_ROWS);
        println!(
            "running index function with params: {}, {}",
            index_x, index_y
        );
        Astar::index(index_x, index_y)
    }

    fn start(&mut self) {
        // go through all objects and assign weights to each node
        for object in &self.objects {
            let (x, y) = object.pos;
            let node_index = Game::index_from_position(x, y).unwrap();
            let node = &mut self.astar.nodes[node_index];
            match &object.object_type {
                ObjectType::Path => node.weight -= 1.0,
                ObjectType::Water => node.weight += 1.0,
                // find start and end indexes
                ObjectType::Start => {
                    self.astar.start_index = node_index;
                    println!(
                        "Found Start object. Changing start_index to: {}",
                        node_index
                    );
                }
                ObjectType::End => self.astar.end_index = node_index,
                _ => (),
            }
            // println!("  pos: ({}, {}); obj: {:?}", x, y, object.object_type);
        }
        // open set starts with start node
        println!(
            "Inserting starting index ({}) into open set...",
            self.astar.start_index
        );
        self.astar.open_set.insert(self.astar.start_index);
        self.astar.run();
    }

    fn save(&self, filename: &str) {
        let path = Path::new(filename);
        let file = File::create(&path).unwrap();
        serde_json::to_writer_pretty(file, &self.objects).unwrap();
        println!("Saved file {}", filename);
    }

    fn load(&mut self, filename: &str) {
        let path = Path::new(filename);
        println!("Path {:?}", path);
        match File::open(path) {
            Ok(file) => {
                let reader = BufReader::new(file);
                let data: Vec<Object> = serde_json::from_reader(reader).unwrap();
                self.objects.clear();
                self.objects = data.clone();
                println!("File {} loaded", filename);
            }
            Err(_) => {
                println!("File {} does not exist", filename);
                return;
            }
        }
    }

    fn draw_nodes(&mut self, ctx: &mut Context, canvas: &mut Canvas) -> GameResult {
        for col in 0..NUM_COLS {
            for row in 0..NUM_ROWS {
                let shape = graphics::Rect {
                    x: 0.0,
                    y: 0.0,
                    w: (WINDOW_WIDTH / NUM_COLS) as f32,
                    h: (WINDOW_HEIGHT / NUM_ROWS) as f32,
                };

                let color = Color::WHITE;
                let rect = Mesh::new_rectangle(ctx, graphics::DrawMode::stroke(1.0), shape, color)?;

                let x = col * WINDOW_WIDTH / NUM_COLS;
                let y = row * WINDOW_HEIGHT / NUM_ROWS;
                canvas.draw(&rect, [x as f32, y as f32]);
            }
        }
        Ok(())
    }

    fn draw_final_path(&mut self, ctx: &mut Context, canvas: &mut Canvas) -> GameResult {
        for node_idx in &self.astar.final_path {
            let (x, y) = Astar::get_center_of_node(*node_idx);

            let circ = Mesh::new_circle(
                ctx,
                graphics::DrawMode::fill(),
                [0.0, 0.0],
                5.0,
                1.0,
                Color::RED,
            )?;
            canvas.draw(&circ, [x, y]);
        }
        Ok(())
    }

    fn draw_thingy(&mut self, ctx: &mut Context, canvas: &mut Canvas) -> GameResult {
        for node_idx in 0..self.astar.nodes.len() {
            let (x, y) = Astar::get_center_of_node(node_idx);

            let circ = Mesh::new_circle(
                ctx,
                graphics::DrawMode::fill(),
                [0.0, 0.0],
                5.0,
                1.0,
                Color::RED,
            )?;
            canvas.draw(&circ, [x, y]);
        }
        Ok(())
    }
}

impl EventHandler for Game {
    fn update(&mut self, ctx: &mut Context) -> GameResult {
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult {
        let mut canvas = Canvas::from_frame(ctx, Palette::green());

        // draw objects
        for obj in &self.objects {
            let color = match obj.object_type {
                ObjectType::Path => Palette::brown(),
                ObjectType::Start => Palette::red(),
                ObjectType::End => Palette::light_green(),
                ObjectType::FinalPath => Color::CYAN,
                ObjectType::Water => Palette::blue(),
            };
            let shape = graphics::Rect {
                x: 0.0,
                y: 0.0,
                w: obj.size,
                h: obj.size,
            };
            let rect = Mesh::new_rectangle(ctx, graphics::DrawMode::fill(), shape, color)?;

            // draw circle over mouse position if mouse is over an object
            if let Some((_obj_type, x, y)) = self.mouse_over_object {
                let circ = Mesh::new_circle(
                    ctx,
                    graphics::DrawMode::stroke(5.0),
                    [0.0, 0.0],
                    10.0,
                    1.0,
                    Color::WHITE,
                )?;
                canvas.draw(&circ, [x, y]);
            }

            canvas.draw(&rect, [obj.pos.0, obj.pos.1]);
        }

        if self.should_draw_nodes {
            self.draw_nodes(ctx, &mut canvas)?;
        }

        if !self.astar.final_path.is_empty() {
            self.draw_final_path(ctx, &mut canvas)?;
        }

        self.draw_thingy(ctx, &mut canvas)?;

        canvas.finish(ctx)?;
        Ok(())
    }

    fn key_down_event(
        &mut self,
        ctx: &mut Context,
        input: ggez::input::keyboard::KeyInput,
        _repeated: bool,
    ) -> GameResult {
        match input.keycode {
            Some(KeyCode::P) => self.current_object_type = ObjectType::Path,
            Some(KeyCode::E) => self.current_object_type = ObjectType::End,
            Some(KeyCode::B) => self.current_object_type = ObjectType::Start,
            Some(KeyCode::W) => self.current_object_type = ObjectType::Water,
            Some(KeyCode::G) => self.should_draw_nodes = !self.should_draw_nodes,
            Some(KeyCode::S) => self.save("savefile.json"),
            Some(KeyCode::L) => self.load("savefile.json"),
            Some(KeyCode::R) => self.start(),
            Some(KeyCode::C) => self.objects.clear(),
            Some(KeyCode::Q) => ctx.request_quit(),
            _ => (),
        }
        Ok(())
    }

    fn mouse_motion_event(
        &mut self,
        _ctx: &mut Context,
        x: f32,
        y: f32,
        _dx: f32,
        _dy: f32,
    ) -> GameResult {
        let (x, y) = (x.round(), y.round());
        if self.mouse_held_down {
            let obj = Object {
                object_type: self.current_object_type,
                pos: (x, y),
                size: NODE_SIZE,
            };

            if !self.objects.contains(&obj) {
                self.objects.push(obj);
            }
        } else {
            // loop over all objects, and find first object that intersects with mouse pos
            for obj in &self.objects {
                if obj.intersects(x, y) {
                    self.mouse_over_object = Some((obj.object_type, x, y));
                    return Ok(());
                }
            }
            self.mouse_over_object = None;
        }

        Ok(())
    }

    fn mouse_button_up_event(
        &mut self,
        _ctx: &mut Context,
        _button: event::MouseButton,
        x: f32,
        y: f32,
    ) -> GameResult {
        self.mouse_held_down = false;
        Ok(())
    }

    fn mouse_button_down_event(
        &mut self,
        _ctx: &mut Context,
        button: event::MouseButton,
        x: f32,
        y: f32,
    ) -> GameResult {
        //println!("Mouse in location ({}, {})", x, y);

        self.mouse_held_down = true;

        Ok(())
    }
}

fn main() {
    let (mut ctx, event_loop) = ContextBuilder::new("Path", "Jose").build().unwrap();

    //ctx.gfx.set_drawable_size(2000.0, 600.0).unwrap();
    println!("Drawable window size: {:?}", ctx.gfx.drawable_size());

    let mut game = Game::new(&mut ctx);
    game.load("savefile.json");
    event::run(ctx, event_loop, game);
}
